package macvlan

import (
	"context"
	"errors"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"gitee.com/iscas-system/cluster/pkg/agent/util"
	"gitee.com/iscas-system/cluster/pkg/daemons/config"
	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
)

func Prepare(ctx context.Context, nodeConfig *config.Node) error {
	logrus.Infof("------------------get bin dir:%v-----------", nodeConfig.AgentConfig.CNIBinDir)
	logrus.Infof("------------------get macvlan cni config dir:%v-----------", nodeConfig.AgentConfig.CNIConfDir)
	if err := createCNIConf(nodeConfig.AgentConfig.CNIConfDir, nodeConfig); err != nil {
		return err
	}

	err := copyCNIConf("", nodeConfig)
	if err != nil {
		logrus.Errorf("=====copy CNI Conf failed:%v===========", err)
		return err
	}
	logrus.Infoln("xxxxxxxxxxxxcopy cni conf successxxxxxxxxxxxxxxx")
	err = copyCNIBin("", nodeConfig)
	return err
}

func copyCNIConf(dir string, nodeConfig *config.Node) error {
	var CNIConfPath string
	if dir == "" {
		CNIConfPath = filepath.Join("/etc", "cni", "net.d", "10-macvlan.conflist")
	} else {
		CNIConfPath = filepath.Join(dir, "10-macvlan.conflist")
	}
	sourceFilePath := filepath.Join(nodeConfig.AgentConfig.CNIConfDir, "10-macvlan.conflist")
	logrus.Infof("++++++sourceFilePath:%v+++++++", sourceFilePath)
	logrus.Infof("+++++++++++++CNIConfPath:%v++++++++", CNIConfPath)
	return util.CopyFile(sourceFilePath, CNIConfPath, false)
}

func createCNIConf(dir string, nodeConfig *config.Node) error {
	logrus.Infof("Creating the CNI conf in directory %s", dir)
	if dir == "" {
		return nil
	}
	p := filepath.Join(dir, "10-macvlan.conflist")

	cniConfJSON := cniConf
	// master 会选择默认网卡

	gw, iface, err := GetDefaultGatewayAndInterface()
	if err != nil {
		return err
	}

	if nodeConfig.MacVlanDefaultInterface == "" {
		cniConfJSON = strings.ReplaceAll(cniConfJSON, "%MASTER%", iface.Name)
	} else {
		cniConfJSON = strings.ReplaceAll(cniConfJSON, "%MASTER%", nodeConfig.MacVlanDefaultInterface)
	}
	cniConfJSON = strings.ReplaceAll(cniConfJSON, "%SCIDR%", nodeConfig.AgentConfig.ClusterCIDR.String())
	cniConfJSON = strings.ReplaceAll(cniConfJSON, "%GATEWAY%", gw)

	number := nodeConfig.NumberStaticIPs
	if nodeConfig.NumberStaticIPs == 0 {
		number = 255
	}

	_, startIP, endIP, err := GetFirstNIPs(nodeConfig.AgentConfig.ClusterCIDR.String(), number)
	if err != nil {
		return err
	}

	cniConfJSON = strings.ReplaceAll(cniConfJSON, "%SRSTART%", startIP)
	cniConfJSON = strings.ReplaceAll(cniConfJSON, "%SREND%", endIP)

	return util.WriteFile(p, cniConfJSON)
}

func copyCNIBin(dir string, nodeConfig *config.Node) error {
	var CNIBinPath string
	if dir == "" {
		CNIBinPath = filepath.Join("/opt", "cni", "bin", "cni")
	} else {
		CNIBinPath = filepath.Join(dir, "cni")
	}

	_, err := os.Stat(CNIBinPath)
	if os.IsNotExist(err) {
		sourceFilePath := filepath.Join(nodeConfig.AgentConfig.CNIBinDir, "cni")
		// logrus.Infof("++++++sourceFileBinPath:%v+++++++", sourceFilePath)
		// logrus.Infof("+++++++++++++CNIBinPath:%v++++++++", CNIBinPath)
		err := util.CopyFile(sourceFilePath, CNIBinPath, false)
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "bandwidth"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "bandwidth"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "bridge"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "bridge"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "firewall"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "firewall"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "flannel"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "flannel"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "host-local"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "host-local"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "loopback"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "loopback"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "portmap"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "portmap"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "macvlan"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "macvlan"))
		if err != nil {
			return err
		}
	}

	_, err = os.Stat(filepath.Join("/opt", "cni", "bin", "static-ipam"))
	if os.IsNotExist(err) {
		err = os.Symlink(CNIBinPath, filepath.Join("/opt", "cni", "bin", "static-ipam"))
		if err != nil {
			return err
		}
	}
	return nil
}

func GetDefaultGatewayAndInterface() (string, *net.Interface, error) {
	routes, err := netlink.RouteList(nil, syscall.AF_INET)
	if err != nil {
		return "", nil, err
	}

	for _, route := range routes {
		if route.Dst == nil || route.Dst.String() == "0.0.0.0/0" {
			if route.LinkIndex <= 0 {
				return "", nil, errors.New("Found default route but could not determine interface")
			}

			iface, err := net.InterfaceByIndex(route.LinkIndex)
			if err != nil {
				return "", nil, errors.New("Found default route but interface not exist")
			}
			return route.Gw.String(), iface, nil
		}
	}

	return "", nil, errors.New("Unable to find default route")
}

func GetFirstNIPs(cidr string, n int) ([]net.IP, string, string, error) {
	ip, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return nil, "", "", errors.New("invalid CIDR block")
	}

	var ips []net.IP
	startIP := ip.String()
	for ip := ip.Mask(ipNet.Mask); ipNet.Contains(ip) && len(ips) < n; incrementIP(ip) {
		ipCopy := make(net.IP, len(ip))
		copy(ipCopy, ip)
		ips = append(ips, ipCopy)
	}
	endIP := ips[len(ips)-1].String()

	return ips, startIP, endIP, nil
}

// Function to increment an IP address
func incrementIP(ip net.IP) {
	for j := len(ip) - 1; j >= 0; j-- {
		ip[j]++
		if ip[j] > 0 {
			break
		}
	}
}
